Skip to content

feat(account): always-visible Renew on yearly licences, deep-linked to checkout#270

Merged
kilbot merged 4 commits into
mainfrom
feat/account-renew-button
Jul 6, 2026
Merged

feat(account): always-visible Renew on yearly licences, deep-linked to checkout#270
kilbot merged 4 commits into
mainfrom
feat/account-renew-button

Conversation

@kilbot

@kilbot kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What

Phase 2 of the manual-renewal work: an always-visible "Renew" button on each renewable (yearly) licence on the account page.

Before, "Renew" only appeared when a licence was expiring-soon (banner) or already expired, and it linked to the generic /pro. Now:

  • Always visible on yearly licences (active or expired).
  • Lifetime licences (null expiry) show no Renew — nothing to renew.
  • Deep-links to the pre-filled yearly checkout /pro/checkout?product=wcpos-pro-yearly (falls back to /pro if the plan handle can't be resolved).

On payment, the existing order-completed subscriber extends the same licence key — no backend change needed here — using the renewal-date rule locked in wcpos-medusa#63 (max(expiry, now) + 1yr: active renewers lose no days, lapsed ones start today).

Details

  • Renewability keys off expiry != null (robust; independent of policy-id resolution).
  • Renew is primary when expired (needed to regain access), secondary/outline when active (optional early renewal).
  • To avoid two identical CTAs, the footer Renew is suppressed while the expiring-soon banner already offers one — so exactly one Renew is ever shown per card. The banner's link now also uses the deep-link.
  • Reuses the existing t('renew') i18n key — no new locale strings, so no 10-locale parity impact.

Validation

  • vitest1206 unit tests (licenses-client: 30, +2: deep-link assertion + lifetime shows-no-renew); updated the "beyond-30-day" test since active non-expiring licences now show Renew.
  • eslint, tsc --noEmit, next build — clean.
  • Not browser-verified: the account/licenses page is auth-gated (needs a real Medusa session + Keygen licence data), so it doesn't render in the local preview without significant mock scaffolding. Behaviour is covered by the component unit tests instead. Worth an eyeball on a real logged-in account after deploy.

Scope

Frontend only. Independent of #264/#265 (attach-customer / save-card) and works with today's checkout. Next up (Phase 3): the one-click off-session charge (consent checkbox → charge saved card) so this button can skip checkout entirely for customers with a saved card.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved renewal CTAs on license cards with smarter, plan-specific checkout links when available.
    • Updated license card actions to show context-appropriate download and renewal options based on license status.
  • Bug Fixes

    • Corrected renewal links for expiring/expired licenses to route to the appropriate yearly checkout URL instead of a generic page.
    • Ensured lifetime licenses do not display a renew option.
  • Tests

    • Expanded unit and end-to-end coverage for renewal link destinations and conditional CTA rendering.

…o checkout

Show a "Renew" button on every renewable (yearly) licence on the account page,
not only when it is expiring-soon or already expired. Lifetime licences (null
expiry) show no Renew — there is nothing to renew.

The button deep-links to the pre-filled yearly checkout
(/pro/checkout?product=<handle>) instead of the generic /pro; on payment the
existing order-completed subscriber extends the SAME licence key using the
locked max(expiry, now) + 1yr rule (see wcpos-medusa#63). Falls back to /pro
when the plan handle cannot be resolved.

Renew is styled primary when expired (needed to regain access) and secondary
when active (an optional early renewal — no days lost). To avoid two identical
CTAs on one card, the footer Renew is suppressed while the expiring-soon banner
already offers one, so exactly one Renew is ever present.

Tests updated for the new always-visible behaviour + a deep-link assertion.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 857bc6a0-3f70-47b3-ace1-dadc328dbc69

📥 Commits

Reviewing files that changed from the base of the PR and between ea97f30 and 6bbe952.

📒 Files selected for processing (3)
  • e2e/license-flows.spec.ts
  • src/components/account/licenses-client.test.tsx
  • src/components/account/licenses-client.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/account/licenses-client.tsx

📝 Walkthrough

Walkthrough

LicensesClient now derives per-license renewal behavior from policy IDs, uses that state to render the expiring-soon Renew link and card CTAs, and updates tests and e2e coverage to match the new checkout deep-linking.

Changes

Renew Link Deep-Linking

Layer / File(s) Summary
Per-license plan lookup and renewHref logic
src/components/account/licenses-client.tsx
Removes the shared getPlanLabel helper and computes planLabel, renewHref, isRenewable, and showRenew per license from getPlanByPolicyId.
Notice and CTA wiring
src/components/account/licenses-client.tsx
The expiring-soon AccountNotice Renew link uses renewHref, and the per-card CTA cluster conditionally renders renew and status-specific download buttons.
Renew link test coverage
src/components/account/licenses-client.test.tsx, e2e/license-flows.spec.ts
Tests assert Renew link presence, absence, and yearly checkout deep-linking across expiring-soon, lifetime, yearly-policy, and expired-license flows.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related issues

Possibly related PRs

  • wcpos/wcpos-com#89: Both PRs modify LicensesClient renewal CTA behavior and related tests in the same account/licenses flow.
  • wcpos/wcpos-com#119: Both PRs touch renewal messaging and Renew CTA logic/tests around the account/licenses flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: always-visible Renew for yearly licences with a direct checkout link.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/account-renew-button

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/components/account/licenses-client.tsx (1)

429-431: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant outer condition.

showRenew is already gated on displayStatus === 'active' || displayStatus === 'expired' (Line 273), so showRenew || in this outer condition never changes the outcome — the wrapper renders whenever displayStatus is 'active' or 'expired', with or without the showRenew disjunct. Simplifying to just the displayStatus check would be clearer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/account/licenses-client.tsx` around lines 429 - 431, The outer
conditional around the renewal UI is redundant because showRenew is already
derived from the active/expired displayStatus logic in licenses-client.tsx.
Update the JSX condition near the wrapper to rely only on displayStatus ===
'active' || displayStatus === 'expired', and remove the unnecessary showRenew
disjunct while keeping the existing rendering behavior unchanged.
src/components/account/licenses-client.test.tsx (1)

264-282: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confirm test policy ID matches DEFAULT_YEARLY_POLICY_ID.

The hardcoded UUID '261cb7e2-6e80-476e-98bd-fe7f406f258d' is asserted to resolve to the yearly plan's handle. This test will silently start failing (or worse, pass for the wrong reason) if DEFAULT_YEARLY_POLICY_ID in src/lib/plans.ts ever changes without updating this literal in lockstep. Consider importing the constant directly rather than duplicating the literal, to keep the test coupled to the source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/account/licenses-client.test.tsx` around lines 264 - 282, The
Renew-link test is duplicating the yearly policy UUID instead of using the
source of truth. Update the test in LicensesClient to reference
DEFAULT_YEARLY_POLICY_ID from src/lib/plans.ts (or otherwise derive the policy
ID from the shared constant) so the assertion stays aligned with the yearly plan
mapping. Keep the existing deep-link expectation in the LicensesClient Renew
button test, but remove the hardcoded UUID literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/account/licenses-client.test.tsx`:
- Around line 264-282: The Renew-link test is duplicating the yearly policy UUID
instead of using the source of truth. Update the test in LicensesClient to
reference DEFAULT_YEARLY_POLICY_ID from src/lib/plans.ts (or otherwise derive
the policy ID from the shared constant) so the assertion stays aligned with the
yearly plan mapping. Keep the existing deep-link expectation in the
LicensesClient Renew button test, but remove the hardcoded UUID literal.

In `@src/components/account/licenses-client.tsx`:
- Around line 429-431: The outer conditional around the renewal UI is redundant
because showRenew is already derived from the active/expired displayStatus logic
in licenses-client.tsx. Update the JSX condition near the wrapper to rely only
on displayStatus === 'active' || displayStatus === 'expired', and remove the
unnecessary showRenew disjunct while keeping the existing rendering behavior
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a0e2916d-4823-4a4b-9f7d-4fe43950a406

📥 Commits

Reviewing files that changed from the base of the PR and between 1aa4f93 and ea97f30.

📒 Files selected for processing (2)
  • src/components/account/licenses-client.test.tsx
  • src/components/account/licenses-client.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea97f307fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/components/account/licenses-client.tsx
Comment thread src/components/account/licenses-client.tsx
@kilbot

kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Fix triage

# Source File Class Category Decision Outcome
1 Codex P2 / CI license-flows.spec.ts:163 Mechanical Blocker (E2E fail) Fix e2e-expired is a yearly policy, so its card Renew now deep-links; update the assertion
2 CodeRabbit licenses-client.tsx:429 Mechanical Nit Fix showRenew already implies active||expired; drop the redundant disjunct
3 CodeRabbit licenses-client.test.tsx:264 Mechanical Nit Fix Import DEFAULT_YEARLY_POLICY_ID instead of the hardcoded UUID (also the existing "Yearly" label test — same literal)
4 Codex P1 licenses-client.tsx:263 Architectural Major Decline Not a regression — renewal-by-repurchase already keys on email + policy, not the clicked card. Per-licence targeting is Phase 3's charge endpoint. Filing a tracking issue.

Working through 1–3 now; detail on #4 in-thread.

kilbot added 3 commits July 6, 2026 15:26
The e2e-expired fixture is a yearly policy, so its card Renew now points at
/pro/checkout?product=wcpos-pro-yearly instead of /pro. Updates the assertion
to match the intended deep-link (Codex P2 / E2E CI failure).
showRenew already implies displayStatus is active|expired, so the outer
condition simplifies to the displayStatus check alone. No behaviour change
(CodeRabbit nitpick).
Import the constant from @/lib/plans instead of duplicating the yearly policy
UUID literal, so the test stays coupled to the source of truth (also the
existing Yearly-label test). CodeRabbit nitpick.
@kilbot

kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Resolution

# Finding Commit Outcome
1 Codex P2 — e2e Renew href (E2E CI fail) cfac102 Fixed — assertion now expects the yearly deep-link
2 CodeRabbit — redundant showRenew disjunct 2c83c38 Fixed — outer gate simplified (no behaviour change)
3 CodeRabbit — hardcoded yearly UUID in test 6bbe952 Fixed — imports DEFAULT_YEARLY_POLICY_ID
4 Codex P1 — multi-licence renewal targeting Declined → tracked in #272 (pre-existing renew-by-email+policy; Phase 3)

Local validation: licenses-client unit tests 30/30, eslint, tsc --noEmit, next build all green. Full Playwright e2e not run locally (heavy cross-worktree setup) — the updated assertion matches the unit-verified deep-link, and E2E re-runs in CI here.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-ibt1w34s7-wcpos.vercel.app

@kilbot kilbot merged commit 5d624c0 into main Jul 6, 2026
9 checks passed
@kilbot kilbot deleted the feat/account-renew-button branch July 6, 2026 13:58
@coderabbitai coderabbitai Bot mentioned this pull request Jul 6, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant